#include <cstdlib>
#include <iostream>
#include <fstream>
#include <sstream>
#include <glad/glad.h>
#include "shader.h"

void checkError(unsigned int shader, GLenum type, const char *name)
{
    int success;
    char infoLog[512];
    glGetShaderiv(shader, type, &success);
    if (!success) {
        glGetShaderInfoLog(shader, 512, NULL, infoLog);
        std::cerr << "ERROR:SHADER::" << name
                  << "_FAILED\n"
                  << infoLog << std::endl;
        exit(1);
    }
}

unsigned int compileShader(const char *vertexSrc, const char *fragmentSrc) {
    // compile vertex shader
    const int nStrings = 1;
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, nStrings, &vertexSrc, NULL);
    glCompileShader(vertexShader);
    checkError(vertexShader, GL_COMPILE_STATUS, "VERTEX::COMPILATION");

    // compile fragment shader
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, nStrings, &fragmentSrc, NULL);
    glCompileShader(fragmentShader);
    checkError(fragmentShader, GL_COMPILE_STATUS, "FRAGMENT::COMPILATION");

    // link (pipe) shaders into shader program
    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    checkError(shaderProgram, GL_LINK_STATUS, "PROGRAM::LINKING");

    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    return shaderProgram;
}

void Shader::init(const char* vertexFile, const char* fragmentFile)
{
    using namespace std;
    string vertexCode, fragmentCode;
    ifstream vertexIfs, fragmentIfs;
    stringstream vertexStream, fragmentStream;

    try {
        // throw exceptions on IO error
        vertexIfs.exceptions(ifstream::failbit | ifstream::badbit);
        vertexIfs.open(vertexFile);
        vertexStream << vertexIfs.rdbuf();
        vertexCode = vertexStream.str();
    } catch (ifstream::failure e) {
        cerr << "ERROR::SHADER::ERROR_READING_VERTEX" << endl;
    }

    try {
        fragmentIfs.exceptions(ifstream::failbit | ifstream::badbit);
        fragmentIfs.open(fragmentFile);
        fragmentStream << fragmentIfs.rdbuf();
        fragmentCode = fragmentStream.str();
        id = compileShader(vertexCode.c_str(), fragmentCode.c_str());
    } catch (ifstream::failure e) {
        cerr << "ERROR::SHADER::ERROR_READING_FRAGMENT" << endl;
    }
}

const Shader &Shader::use() const
{
    glUseProgram(id);
    return *this;
}

const Shader &Shader::set(const std::string &name, bool value) const
{
    glUniform1i(glGetUniformLocation(id, name.c_str()), (int)value);
    return *this;
}

const Shader &Shader::set(const std::string &name, int value) const
{
    glUniform1i(glGetUniformLocation(id, name.c_str()), value);
    return *this;
}

const Shader &Shader::set(const std::string &name, float value) const
{
    glUniform1f(glGetUniformLocation(id, name.c_str()), value);
    return *this;
}

const Shader &Shader::set(const std::string &name, float v1, float v2, float v3, float v4) const
{
    glUniform4f(glGetUniformLocation(id, name.c_str()), v1, v2, v3, v4);
    return *this;
}

const Shader &Shader::set(const std::string &name, const glm::mat4 &mat) const
{
    glUniformMatrix4fv(glGetUniformLocation(id, name.c_str()), 1, GL_FALSE, glm::value_ptr(mat));
    return *this;
}
